NestJS API Versioning With Swagger

Written by Chat GPT Deep Research

NestJS API 버저닝과 Swagger 연동

NestJS는 REST API를 효율적으로 구축할 수 있도록 API 버전 관리(Versioning) 기능을 제공합니다. 또한 @nestjs/swagger 모듈을 사용하면 API 문서를 손쉽게 자동 생성할 수 있습니다. 이 글에서는 URI 기반의 API 버저닝을 NestJS에서 구현하고 Swagger(OpenAPI) 문서화와 통합하는 방법을 살펴보겠습니다. 실무 예제를 통해 단일 Swagger 문서로 모든 버전을 관리하는 방법과, 다중 Swagger 문서로 버전별(또는 모듈별)로 문서를 분리하는 두 가지 전략을 구현하고 비교해보겠습니다.

URI 기반 API 버전 관리 설정 (main.ts)

NestJS에서는 간단한 설정으로 URI에 버전 정보를 포함시킬 수 있습니다. URI 버전 관리는 URL 경로에 /v1, /v2처럼 버전 세그먼트를 추가하여 버전을 구분하는 방식입니다. 우선 main.ts 부트스트랩 파일에서 버전 관리 기능을 활성화해야 합니다. Nest 애플리케이션 인스턴스에 대해 app.enableVersioning()을 호출하며, type: VersioningType.URI 옵션을 지정하면 URI 기반 버저닝이 적용됩니다.

특히 중요한 점은 Swagger 문서를 설정하기 전에 먼저 enableVersioning을 호출해야 한다는 것입니다. 이는 NestJS Swagger 모듈의 이슈에서도 강조된 부분으로, 순서가 바뀌면 Swagger가 버전이 붙은 경로들을 인식하지 못할 수 있습니다. 다음은 main.ts의 예시 코드입니다:

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { VersioningType } from '@nestjs/common';
import { setupSwagger } from './setupSwagger';  // Swagger 설정 함수 import

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // (중요) Swagger 설정 전에 API 버전 관리 활성화
  app.enableVersioning({ type: VersioningType.URI });

  // Swagger 문서 설정
  setupSwagger(app);

  await app.listen(3000);
}
bootstrap();

위 코드에서 app.enableVersioning({ type: VersioningType.URI }) 한 줄로 전역적인 URI 버전 관리가 활성화됩니다. 이제 컨트롤러나 라우트에 버전 메타데이터를 달면 요청 경로에 해당 버전이 자동으로 추가됩니다 (예: /v1/..., /v2/...).

버전별 컨트롤러 구현 (예: ReservationV2Controller)

이제 특정 컨트롤러에 버전을 지정하여 버전에 따라 다른 동작을 하도록 구성할 수 있습니다. NestJS에서는 컨트롤러나 개별 경로 핸들러에 @Version() 데코레이터를 사용하거나, 컨트롤러 데코레이터에 { version: '값' } 옵션을 줘서 해당 버전에서만 동작하도록 만들 수 있습니다. 예를 들어 v2 버전의 API 엔드포인트를 담당하는 ReservationV2Controller를 구현하면 다음과 같습니다:

// reservation.v2.controller.ts
import { Controller, Get, Post, Param } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';

@Controller({ path: 'reservations', version: '2' })
@ApiTags('Reservations')
export class ReservationV2Controller {
  @Get()
  @ApiOperation({ summary: '모든 예약 목록 조회 (v2)' })
  findAllV2() {
    // 예약 목록 조회 v2 로직...
  }

  @Get(':id')
  @ApiOperation({ summary: 'ID로 예약 조회 (v2)' })
  findByIdV2(@Param('id') id: string) {
    // 특정 ID 예약 조회 v2 로직...
  }

  @Post()
  @ApiOperation({ summary: '새 예약 생성 (v2)' })
  createV2() {
    // 예약 생성 v2 로직...
  }
}

위 코드에서는 @Controller({ path: 'reservations', version: '2' })를 통해 이 컨트롤러의 모든 경로가 v2로 버전이 지정되었습니다. 따라서 예를 들어 findAllV2() 메서드는 GET 요청으로 /v2/reservations 경로를 처리하게 됩니다. (ReservationV1Controller에서는 version을 '1'로 지정하거나 기본값을 사용해 /v1/reservations를 처리하도록 구현하면 됩니다.) 각 컨트롤러에 @ApiTags 등을 지정하면 Swagger 문서에서 태그로 구분할 수도 있습니다.

이렇게 각 버전별로 별도 컨트롤러를 구성하면 버전 상승에 따른 변경 사항을 새로운 컨트롤러/메서드로 구현할 수 있어 관리가 용이합니다. NestJS는 요청이 들어올 때 URI의 버전에 따라 해당 버전을 지원하는 컨트롤러와 핸들러를 찾아주므로, 클라이언트는 /v1/... 또는 /v2/...와 같이 원하는 버전을 명시하여 호출하면 됩니다.

Swagger 설정 - 단일 문서 통합

이제 Swagger를 설정하여 버전이 적용된 API 엔드포인트들을 문서화해보겠습니다. 앞서 enableVersioning()먼저 호출했기 때문에, 이제 Swagger 문서를 생성하면 각 경로에 자동으로 버전 prefix가 포함된 상태로 스펙이 만들어집니다. 한 가지 접근 방식은 **"단일 Swagger 문서"**를 생성하여 모든 버전의 API를 한꺼번에 보여주는 것입니다. 소규모 서비스나 API 변경 범위가 크지 않은 경우, 한 개의 Swagger UI에서 v1과 v2 엔드포인트를 모두 열람할 수 있어 편리합니다.

단일 문서 전략에서는 NestJS 애플리케이션 전체를 대상으로 한 번의 SwaggerModule.createDocument() 호출로 문서를 생성합니다. DocumentBuilder를 이용해 기본적인 정보(title, description, version 등)를 설정하고 문서를 만든 뒤, SwaggerModule.setup()으로 Swagger UI 엔드포인트를 등록합니다. 앞서 main.ts 예시에서는 setupSwagger(app) 함수를 별도로 분리했는데, 그 구현은 다음과 같습니다:

// setupSwagger.ts
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { INestApplication } from '@nestjs/common';

export function setupSwagger(app: INestApplication) {
  // Swagger 설정 생성
  const config = new DocumentBuilder()
    .setTitle('Reservation API')
    .setDescription('Reservation 서비스 API 문서')
    .setVersion('1.0')
    .build();

  // 애플리케이션 전체를 대상으로 Swagger 문서 생성 (모든 버전 포함)
  const document = SwaggerModule.createDocument(app, config);

  // Swagger UI 엔드포인트 설정 (예: http://localhost:3000/docs)
  SwaggerModule.setup('docs', app, document);
}

위 설정으로 애플리케이션의 모든 등록된 경로를 스캔하여 OpenAPI 문서(document 객체)를 생성합니다. 이 문서에는 현재 애플리케이션에 존재하는 v1, v2 모든 버전의 엔드포인트가 포함됩니다. 예를 들어 v1과 v2 두 가지 버전의 reservations 경로들이 각각 /v1/reservations, /v2/reservations로 문서화되어 한 Swagger UI에서 모두 확인할 수 있습니다. Swagger UI는 위 예시에서 /docs 경로로 제공되므로, 브라우저에서 http://<서버주소>/docs로 접속하면 됩니다.

참고: 전역 경로(prefix) app.setGlobalPrefix()를 사용해 /api/v1 형식으로 경로를 구성하는 경우, Swagger 설정 시 전역 프리픽스를 무시하거나 경로를 조정해야 합니다. SwaggerModule.createDocument의 옵션으로 ignoreGlobalPrefix: true를 주면 전역 프리픽스를 문서에서 생략할 수 있고, 또는 SwaggerModule.setup()의 경로에 :version과 같은 동적 세그먼트를 사용해 Swagger UI를 각 버전에 맞게 제공할 수도 있습니다. 단일 문서 접근 방식에서는 일반적으로 한 개의 UI에서 모든 버전을 보여주지만, 필요에 따라 이러한 설정으로 Swagger UI 접근 경로를 튜닝할 수 있습니다.

Swagger 설정 - 다중 문서 분리

API 규모가 커지거나, 모노레포(monorepo) 구조로 여러 모듈이 공존하는 경우 혹은 마이크로서비스 경계를 나눠 별도의 API 문서를 관리해야 하는 경우에는 "다중 Swagger 문서" 전략이 유용합니다. 이 접근 방식에서는 API 버전별 또는 모듈별로 별도의 Swagger 스펙과 UI를 만들어 제공합니다. 예를 들어, v1 API는 /v1/docs에서 문서화하고 v2 API는 /v2/docs에서 별도 UI로 제공할 수 있습니다 (혹은 예약 서비스결제 서비스를 각각 다른 문서로 노출하는 식으로 모듈별 분리도 가능). 이렇게 하면 특정 버전이나 도메인에 해당하는 엔드포인트만 선별하여 문서화할 수 있어 문서가 방대해지는 것을 방지하고 관리 포인트를 분리할 수 있습니다.

NestJS Swagger 모듈은 이러한 다중 명세 지원을 기본 제공하는데, 이를 위해 애플리케이션을 모듈화하고 SwaggerModule.createDocument() 호출 시 include 옵션을 활용하면 됩니다. createDocument의 세 번째 인자로 SwaggerDocumentOptions 객체를 전달하여, 그 안에 포함시킬 모듈 목록을 include 배열로 지정하면 해당 모듈들에 속한 경로만을 스캔하여 문서를 생성합니다. 공식 문서의 예제를 보면 CatsModule과 DogsModule을 개별 포함하여 두 개의 스펙을 만들고, 각각 별도의 Swagger UI 엔드포인트에 노출할 수 있다고 설명합니다.

다음은 버전별로 모듈을 분리하여 Swagger 문서를 생성하는 코드 예시입니다:

// 예시: v1, v2 모듈이 분리된 경우의 Swagger 문서 설정
import { ReservationsModuleV1 } from './reservations-v1.module';
import { ReservationsModuleV2 } from './reservations-v2.module';

// ... (NestFactory로 app 생성 등은 생략)

const v1Config = new DocumentBuilder()
  .setTitle('Reservation API - v1')
  .setDescription('Reservation API (버전 1)')
  .setVersion('1.0')
  .build();

// v1 모듈만 포함한 Swagger 문서 생성
const documentV1 = SwaggerModule.createDocument(app, v1Config, {
  include: [ReservationsModuleV1],
});
SwaggerModule.setup('api/v1/docs', app, documentV1);  // v1 문서 UI 엔드포인트

const v2Config = new DocumentBuilder()
  .setTitle('Reservation API - v2')
  .setDescription('Reservation API (버전 2)')
  .setVersion('2.0')
  .build();

// v2 모듈만 포함한 Swagger 문서 생성
const documentV2 = SwaggerModule.createDocument(app, v2Config, {
  include: [ReservationsModuleV2],
});
SwaggerModule.setup('api/v2/docs', app, documentV2);  // v2 문서 UI 엔드포인트

위 코드에서는 ReservationsModuleV1ReservationsModuleV2 두 모듈을 가정하고, 각각의 모듈을 포함하는 별도의 Swagger 문서를 만들었습니다. include 옵션에 배열로 모듈을 넘기면 해당 모듈 및 그 하위에 연결된 모든 컨트롤러 경로들만 반영된 OpenAPI 스펙이 생성됩니다. 이렇게 생성된 documentV1, documentV2를 서로 다른 경로(/api/v1/docs, /api/v2/docs)에 SwaggerModule.setup()으로 설정하여, 버전 1용 Swagger UI버전 2용 Swagger UI를 분리해 제공합니다. 이제 클라이언트나 개발자는 /api/v1/docs에서 v1 API만, /api/v2/docs에서 v2 API만 각각 확인할 수 있습니다.

팁: 이 접근 방식은 버전뿐 아니라 기능별 모듈로 문서를 분리하는 경우에도 활용할 수 있습니다. 예를 들어 _Public API_와 _Admin API_를 각각 다른 모듈로 관리하면서 Swagger 문서를 별도로 제공하거나, 마이크로서비스별로 문서를 따로 생성하여 서비스 경계마다 문서를 참조하게 할 수 있습니다. include 옵션에 원하는 모듈 조합을 넘기면 필요한 엔드포인트 그룹만 문서화할 수 있습니다.

각 접근 방식의 활용 시나리오

앞서 살펴본 단일 문서 통합다중 문서 분리 두 가지 Swagger 통합 전략은 각각 장단점이 있으며, 사용할 상황이 다릅니다. 프로젝트의 규모와 요구 사항에 따라 알맞은 방식을 선택하면 됩니다:

요약하면, 작은 규모나 단일 서비스라면 모든 버전을 하나의 Swagger 문서로 관리하는 편이 간편하며, 대규모 서비스나 다수의 모듈을 갖는 경우에는 버전/모듈별로 Swagger 문서를 분리하여 유지보수성과 명확성을 높이는 것이 좋습니다. 두 접근 방식 모두 NestJS에서 공식적으로 지원되므로, 필요에 따라 적절히 선택하여 활용하면 됩니다.

각 방법을 구현하는 코드는 위에서 살펴본 대로 비교적 간단하며, NestJS의 버전 관리 및 Swagger 모듈 기능을 조합하여 유연하게 API 문서를 구성할 수 있습니다. 올바르게 설정만 한다면, NestJS는 버전별로 호환성 유지문서화를 손쉽게 지원하므로, 안심하고 API를 발전시켜 나갈 수 있을 것입니다.

참고 자료: NestJS 공식 문서 및 GitHub 이슈 등을 통해 API 버저닝 및 Swagger 통합에 대한 추가 정보를 얻을 수 있습니다. (예: NestJS Docs - Versioning, OpenAPI/Swagger 섹션) 이번 가이드에서 다룬 내용은 현업에서도 많이 사용하는 패턴으로, NestJS 기반 프로젝트의 API 설계와 문서 전략 수립에 도움이 되길 바랍니다.